/*
 * Project it.battlehorse.rcp.sl
 * Created on Apr 9, 2006
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * A copy of the LGPL is available also at
 * http://www.gnu.org/copyleft/lesser.html
 * 
 */ 
package it.battlehorse.rcp.sl;

import it.battlehorse.rcp.sl.annot.Injected;
import it.battlehorse.rcp.sl.internal.ServiceFactoryDescriptor;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.Platform;

/**
 * Defines the service locator which is responsible for injecting dependencies into serviceable objects.
 * The service locator perform the work by keeping a registry of service factories, which can serve
 * instances of specific class types, and injecting these service instances into serviceable objects.
 * 
 * @author Riccardo "battlehorse" Govoni [battlehorse@gmail.com]
 * @since Apr 9, 2006
 */
public class ServiceLocator {
	
	private static boolean DEBUG ;	
	
	/**
	 * Contains the shared instance of the service locator
	 */
	private static ServiceLocator instance;
	
	static {
		String slDebug = System.getProperty("service.locator.debug","false");
		DEBUG = slDebug.equalsIgnoreCase("true");
	}
	
	/**
	 * Returns the shared instance of the class. This method should not be invoked
	 * directly : the service locator should be accessed using the plugin activator class.
	 * 
	 * @return the shared service locator instance
	 */
	public static synchronized ServiceLocator getInstance() {
		if (instance == null)
			instance =  new ServiceLocator();
		
		return instance;
	}
	
	/**
	 * Injects the requested dependencies into the parameter object. It scans the serviceable object
	 * looking for methods tagged with the {@link Injected} annotation. Parameter types are extracted from the
	 * matching method. An instance of each type is created from the registered factories ( see {@link IServiceFactory} ).
	 * When instances for all the parameter types have been created the method is invoked and the
	 * next one is serviced.  
	 *  
	 * @param serviceable the object to be serviced
	 * @throws ServiceException
	 */
	public static void service(Object serviceable) throws ServiceException {
			
		ServiceLocator sl = getInstance();
		if (sl.isAlreadyServiced(serviceable))	{	// prevent multiple initializations due to constructor hierarchyes
			if (DEBUG) System.out.println("SL: Object " + serviceable + " has already been serviced ");
			return;
		}
		
		if (DEBUG) System.out.println("SL: Servicing " + serviceable);		
						
		// Parse the class for the requested services
		for (Method m : serviceable.getClass().getMethods()) {
			boolean skip = false;
			Injected ann = m.getAnnotation(Injected.class);
			if (ann != null) {				
				Object[] services = new Object[m.getParameterTypes().length];
				int i = 0;
				
				for (Class<?> klass : m.getParameterTypes()) {
					IServiceFactory factory = sl.getFactory(klass,ann.optional());
					if (factory == null) {
						skip = true;
						break;
					}
					Object service =  factory.getServiceInstance(serviceable);
					if (service == null)
						service = factory.getServiceInstance();
					
					if (service == null) {
						if (ann.optional()) {
							skip = true;
							break;
						}
						else
							throw new ServiceException("Unable to get service instance from " + factory);
							
					}
						
					
					// sanity check: verify that the returned service's class is the expected one from the method
					assert(service.getClass().equals(klass) || klass.isAssignableFrom(service.getClass()));
					
					services[i++]  = service ;
				}
			
				try {
					if (!skip) m.invoke(serviceable, services);
				}
				catch(IllegalAccessException iae) {
					if (!ann.optional())
						throw new ServiceException("Unable to initialize services on " + serviceable + ": " + iae.getMessage(),iae);
				}
				catch(InvocationTargetException ite) {
					if (!ann.optional())
						throw new ServiceException("Unable to initialize services on " + serviceable + ": " + ite.getMessage(),ite);
				}
			}
		}
		
		sl.setAsServiced(serviceable);
	}
	
	//
	// Instance field and methods
	//
	
	/* The service factory registry */
	private Map<ServiceFactoryDescriptor,IServiceFactory> factories;
	
	/* A map which associates class names with factories which can serve them */ 
	private Map<String,ServiceFactoryDescriptor> services;
	
	/* A set of the already serviced objects */
	private WeakHashMap<Object,Object> servicedObjects;
	
	/**
	 * Private constructor
	 *
	 */
	private ServiceLocator() {
		
		factories = new ConcurrentHashMap<ServiceFactoryDescriptor, IServiceFactory>();
		services = new ConcurrentHashMap<String, ServiceFactoryDescriptor>();
		servicedObjects = new WeakHashMap<Object,Object>();
		
		IExtension[] extensions = Platform.getExtensionRegistry().getExtensionPoint("it.battlehorse.rcp.sl.service").getExtensions();
		if (extensions != null)
    		for (int i = 0; i < extensions.length; i++) {
    			
    			IConfigurationElement[] confs = extensions[i].getConfigurationElements();
    			for (int j = 0; j < confs.length ; j++) {
    				IConfigurationElement conf = confs[j];
    				if (conf.getName().equals("serviceFactory")) {
    					ServiceFactoryDescriptor descriptor = new ServiceFactoryDescriptor(conf);
    					services.put(descriptor.getResourceClass(),descriptor);    						
    				}
    			}
    		}		
	}
	
	/**
	 * Returns the factory which can provide instances of the specified class
	 * 
	 * @param resourceClass the class which is being requested 
	 * @param optional defines if a missing factory for the requested class will result in a {@code ServiceException}
	 * 		or simply ignored 
	 * @return a factory which can serve instances of the requested class
	 * @throws ServiceException if a matching factory is not found and the requested class is not optional
	 */
	private IServiceFactory getFactory(Class<?> resourceClass, boolean optional) throws ServiceException {
		ServiceFactoryDescriptor descriptor = services.get(resourceClass.getName());
		if (descriptor == null) {
			if (optional)
				return null;
			else
				throw new ServiceException("No factory available for services of kind " + resourceClass.getName());
		}			
		
		IServiceFactory factory = null;
		synchronized(this) {
			factory = factories.get(descriptor);
			if (factory == null) {
				factory = descriptor.getFactoryInstance();
				factories.put(descriptor, factory);
			}
		}
		
		return factory;
	}
	
	/**
	 * Returns whether the parameter objects has already been serviced.
	 * 
	 * @param serviceable the object which is being serviced
	 * @return whether the parameter objects has already been serviced.
	 */
	private boolean isAlreadyServiced(Object serviceable) {
		return servicedObjects.keySet().contains(serviceable);
	}
	
	/**
	 * Marks the parameter object as already serviced
	 * 
	 * @param serviceable
	 */
	private void setAsServiced(Object serviceable) {
		servicedObjects.put(serviceable, null);
	}

}
